package bsh;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.math.MathContext;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import bsh.compilation.DebugOptions;

/**
 * Classe que deve servir de base para todos os scripts compilados. Tem suporte a invocacoes dinamicas e campos dinamicos.
 * 
 * @author takeshi
 */
public abstract class Prototype implements CompiledScript {

    private final Map<String, Field> fields = new HashMap<String, Field>();

    private final Map<String, Object> dynamic = new HashMap<String, Object>();

    private MathContext mathContext = MathContext.DECIMAL64;

    private final NameSpace rootNS = new NameSpace(new BshClassManager(), "root");

    private final CallStack rootCallStack = new CallStack(this.rootNS);

    protected CallStack callstack = this.rootCallStack;

    protected NameSpace namespace = this.rootNS;

    {
        Field[] fsArray = this.getClass().getFields();
        for (Field field : fsArray) {
            field.setAccessible(true);
            this.fields.put(field.getName(), field);
        }
    }

    /**
     * Seta valores dos fields
     * 
     * @param map Map
     */
    public void setFields(Map<String, Object> map) {
        for (Entry<String, Object> entry : map.entrySet()) {
            this.setField(entry.getKey(), entry.getValue());
        }
    }

    /**
     * Reseta os fields para que esta instancia seja reutilizada
     */
    public void resetAllFields() {
        this.dynamic.clear();
        Collection<Field> values = this.fields.values();
        for (Field field : values) {
            if (CompiledScriptHelper.isInstanceAssignable(field)) {
                try {
                    field.set(this, PrimitiveUtils.getDefaultValue(field.getType()));
                } catch (IllegalAccessException e) {
                    throw new IllegalStateException(e);
                }
            }
        }
    }

    /**
     * Reseta um field, colocando um valor padrao
     * 
     * @param name String
     */
    public void resetField(String name) {
        try {
            Field field = this.fields.get(name);
            if (CompiledScriptHelper.isInstanceAssignable(field)) {
                field.set(this, PrimitiveUtils.getDefaultValue(field.getType()));
            } else {
                this.dynamic.remove(name);
            }
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * @param name String
     * @param value Object
     */
    public void setField(String name, Object value) {
        try {
            Field field = this.fields.get(name);
            if (CompiledScriptHelper.isInstanceAssignable(field)) {
                field.set(this, value);
            } else {
                this.setDynamicField(name, value);
            }
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
        if ((this.debugFlags() & (1 << DebugOptions.VIRTUAL_STACK_FRAMES.ordinal())) != 0) {
            try {
                this.rootCallStack.top().setVariable(name, value == null ? Primitive.VOID : value, false);
            } catch (UtilEvalError e) {
                // something changed in the internal API and needs to be fixed
                throw new RuntimeException(e);
            }
        }
    }

    /**
     * Pega o valor de um campo.
     * 
     * @param name String
     * @return Object
     */
    public final Object getFieldValue(String name) {
        try {
            Field f = this.fields.get(name);
            if (f == null) {
                return this.getDynamicField(name);
            }
            return f.get(this);
        } catch (IllegalAccessException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Callback para executar o corpo do script
     * 
     * @return Object
     * @throws Exception e
     */
    public abstract Object executeScript() throws Exception;

    /**
     * Devolve um field dinamico
     * 
     * @param name String
     * @return Object
     */
    public Object getDynamicField(String name) {
        return this.dynamic.get(name);
    }

    /**
     * Seta um field dinamico
     * 
     * @param name String
     * @param value Object
     */
    public void setDynamicField(String name, Object value) {
        this.dynamic.put(name, value);
    }

    /**
     * Cria um objeto baseado nos tipos dinamicos
     * 
     * @param callsiteId int
     * @param type class
     * @param args args
     * @return Object
     * @throws Throwable t
     */
    public Object createObject(int callsiteId, Class<?> type, Object[] args) throws Throwable {
        return CompiledScriptHelper.createObject(callsiteId, type, args, this);
    }

    /**
     * Faz uma invocacao dinamica
     * 
     * @param callsiteId int
     * @param invocationTarget Object
     * @param methodName String
     * @param args Arguments
     * @return Object
     * @throws Throwable e
     */
    public Object invokeDynamic(int callsiteId, Object invocationTarget, String methodName, Object[] args) throws Throwable {
        return CompiledScriptHelper.invokeDynamic(invocationTarget, callsiteId, methodName, args, this);
    }

    /**
     * @return the mathContext
     */
    public MathContext getMathContext() {
        return this.mathContext;
    }

    /**
     * @param mathContext the mathContext to set
     */
    public void setMathContext(MathContext mathContext) {
        this.mathContext = mathContext;
    }

    /**
     * 
     */
    public final void popStack() {
        this.callstack.pop();
    }

    /**
     * 
     */
    public final void popMethod() {
        NameSpace ns = this.callstack.top();
        while (!ns.getName().startsWith("[METHOD ")) {
            this.callstack.pop();
            ns = this.callstack.top();
        }
    }

    /**
     * @param name String
     */
    public final void newStackFrame(String name) {
        this.callstack.push(new NameSpace(this.callstack.top(), name));
    }

    /**
     * @return int as flags de debug
     */
    public abstract int debugFlags();

    /**
     * @param name String
     * @param value Object
     */
    public final void setLocalVariable(String name, Object value) {
        try {
            Object wrapped = value == null ? Primitive.VOID : value;
            this.callstack.top().setVariable(name, wrapped, false);
        } catch (UtilEvalError e) {
            // ignore this error
        }
    }

    /**
     * @return {@link CallStack}
     */
    public final CallStack getRootCallStack() {
        return this.rootCallStack;
    }

    /**
     * @return {@link NameSpace}
     */
    public final NameSpace getRootNS() {
        return this.rootNS;
    }

    /**
     * @param value double
     * @return {@link BigDecimal}
     */
    public BigDecimal localToBigDecimal(double value) {
        return new BigDecimal(String.valueOf(value), this.getMathContext());
    }

}
